package com.katesoft.scale4j.persistent.client;

import com.katesoft.scale4j.common.services.IBeanNameReferences;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.enums.CrudType;
import com.katesoft.scale4j.persistent.model.RevisionDomainEntity;
import com.katesoft.scale4j.persistent.model.unified.AbstractPersistentEntity;
import com.katesoft.scale4j.persistent.types.RevisionData;
import net.jcip.annotations.ThreadSafe;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.util.Version;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.DefaultRevisionEntity;
import org.hibernate.envers.RevisionType;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.AuditQuery;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.perf4j.aop.Profiled;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.orm.hibernate3.HibernateCallback;

import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import static com.katesoft.scale4j.common.utils.AssertUtility.assertNotNull;
import static org.hibernate.envers.AuditReaderFactory.get;

/**
 * Default implementation of IHibernateDaoSupport interface.
 * <p/>
 * jvmcluster-persistent provides audit capability and can track history of domain entity (hibernate envers module is used for this purpose, so there are no
 * specific requirements for target client to be able to use historical management).
 * <p/>
 * Also entity indexing capability included as well(hibernate search is used for this).
 * <p/>
 * This class contains few method for historical browsing.
 * <p/>
 * Method of this class marked with Profiled annotation, so runtime statistic can be gathered(users would need to define profiled aspect or use
 * jvmcluster-rttp
 * module).
 *
 * @author kate2007
 */
@ThreadSafe
public class LocalHibernateDaoSupport implements IHibernateDaoSupport
{
    private Logger logger = LogFactory.getLogger(getClass());
    private LocalHibernateTemplate hibernateTemplate;

    @Profiled(tag = "dao.support.search_{$1.name}",
              message = "search called with parameters[queryString={$0} class={$1.name} properties={$2}]")
    public <T extends AbstractPersistentEntity> List<T> search(final String queryString,
                                                               final Class<T> entityClass,
                                                               final String[] properties)
    {
        return hibernateTemplate.execute(new HibernateCallback<List<T>>()
        {
            @SuppressWarnings("unchecked")
            @Override
            public List<T> doInHibernate(final Session session) throws HibernateException
            {
                if (StringUtils.isBlank(queryString)) {
                    final Criteria criteria = session.createCriteria(entityClass);
                    hibernateTemplate.prepareCriteria(criteria);
                    return criteria.list();
                }
                String _queryString = queryString.trim();
                if (!_queryString.endsWith("*")) {
                    _queryString += "*";
                }
                FullTextSession fullTextSession = Search.getFullTextSession(session);
                QueryParser parser = new MultiFieldQueryParser(Version.LUCENE_30, properties, new StandardAnalyzer(Version.LUCENE_30));
                parser.setAllowLeadingWildcard(true);
                parser.setDefaultOperator(QueryParser.Operator.AND);
                org.apache.lucene.search.Query luceneQuery;
                try {
                    luceneQuery = parser.parse(_queryString);
                    org.hibernate.Query query = fullTextSession.createFullTextQuery(luceneQuery, entityClass);
                    hibernateTemplate.prepareQuery(query);
                    return query.list();
                }
                catch (ParseException e) {
                    throw new HibernateException(e);
                }
            }
        });
    }

    @SuppressWarnings({"unchecked"})
    @Profiled(tag = "dao.support.loadForRevision_{$0.name}",
              message = "loadForRevision called with parameters [class={$0.name} uid={$1} revision={$2}]")
    protected <T extends AbstractPersistentEntity> T loadForRevision(final Class<T> entityClass,
                                                                     final long uniqueIdentifier,
                                                                     final Number revisionNumber)
    {
        return (T) hibernateTemplate.execute(new HibernateCallback<Object>()
        {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException
            {
                T result = get(session).find(entityClass, uniqueIdentifier, revisionNumber);
                if (result != null) {
                    result.forceAttributesLoad();
                    logger.debug("entity %s.%s loaded for revision = %s", entityClass.getSimpleName(), uniqueIdentifier, revisionNumber);
                }
                return result;
            }
        });
    }

    @Override
    @Profiled(tag = "dao.support.latestRevision_{$0.name}",
              message = "latestRevision called with parameters[class={$0.name} uid={$1}]")
    public <T extends AbstractPersistentEntity> Number latestRevision(final Class<T> entityClass,
                                                                      final long uniqueIdentifier)
    {
        return hibernateTemplate.execute(new HibernateCallback<Number>()
        {
            @Override
            public Number doInHibernate(Session session) throws HibernateException, SQLException
            {
                try {
                    Number number = (Number) get(session).createQuery().
                        forRevisionsOfEntity(entityClass, true, true).
                        addProjection(AuditEntity.revisionNumber().max()).
                        add(AuditEntity.id().eq(uniqueIdentifier)).getSingleResult();
                    if (number != null) {
                        logger.debug("max_rev = %s for entity_class %s.%s", number, entityClass.getSimpleName(), uniqueIdentifier);
                    }
                    return number;
                }
                catch (javax.persistence.NoResultException e) {
                    logger.debug("unable to find revision number by[entity_class=%s,unique_identifier=%s]", entityClass.getName(), uniqueIdentifier);
                }
                return null;
            }
        });
    }

    @Override
    @Profiled(tag = "dao.support.allRevisions_{$0.name}",
              message = "allRevisions called with parameters[class={$0.name} uid={$1}]")
    public <T extends AbstractPersistentEntity> List<Number> allRevisions(final Class<T> entityClass,
                                                                          final long uniqueIdentifier)
    {
        return hibernateTemplate.execute(new HibernateCallback<List<Number>>()
        {
            @SuppressWarnings({"unchecked"})
            @Override
            public List<Number> doInHibernate(Session session) throws HibernateException, SQLException
            {
                List<Number> l = get(session).createQuery().
                    forRevisionsOfEntity(entityClass, true, true).
                    add(AuditEntity.id().eq(uniqueIdentifier)).
                    addProjection(AuditEntity.revisionNumber()).
                    addOrder(AuditEntity.revisionNumber().desc()).getResultList();
                if (l != null) {
                    logger.debug("revisions %s for entity_class %s.%s", l, entityClass.getSimpleName(), uniqueIdentifier);
                }
                return l;
            }
        });
    }

    @Override
    @Profiled(tag = "dao.support.loadAllRevisions_{$0.name}",
              message = "loadAllRevisions called with parameters[class={$0.name} uid={$1}]")
    public <T extends AbstractPersistentEntity> List<RevisionData<T>> loadAllRevisions(final Class<T> entityClass,
                                                                                       final long uniqueIdentifier)
    {
        return hibernateTemplate.execute(new HibernateCallback<List<RevisionData<T>>>()
        {
            @SuppressWarnings({"unchecked"})
            @Override
            public List<RevisionData<T>> doInHibernate(Session session) throws HibernateException, SQLException
            {
                List<RevisionData<T>> result = new LinkedList<RevisionData<T>>();
                List list = get(session).createQuery().forRevisionsOfEntity(entityClass, false, true).
                    add(AuditEntity.id().eq(uniqueIdentifier)).
                    addOrder(AuditEntity.revisionNumber().desc()).getResultList();
                for (Object o : list) {
                    Object[] revisionsData = (Object[]) o;
                    T entity = (T) revisionsData[0];
                    entity.forceAttributesLoad();
                    result.add(new RevisionData<T>(entity, revisionsData[1], CrudType.from((RevisionType) revisionsData[2])));
                }
                return result;
            }
        });
    }

    @Override
    @Profiled(tag = "dao.support.latestRevisionForDate_{$0.name}",
              message = "latestRevisionForDate called with parameters[class={$0.name} date={$1} uid={$2}]")
    public <T extends AbstractPersistentEntity> Map<Long, RevisionData<T>> latestRevisionForDate(final Class<T> entityClass,
                                                                                                 final Date date,
                                                                                                 final Collection<Long> uniqueIdentifiers,
                                                                                                 final String prop)
    {
        assertNotNull(date, "revision date can't be null");
        return hibernateTemplate.execute(new HibernateCallback<Map<Long, RevisionData<T>>>()
        {
            @SuppressWarnings({"unchecked"})
            @Override
            public Map<Long, RevisionData<T>> doInHibernate(Session session) throws HibernateException, SQLException
            {
                AuditReader auditReader = get(session);
                Map<Long, RevisionData<T>> result = new LinkedHashMap<Long, RevisionData<T>>(uniqueIdentifiers.size());
                for (Long id : uniqueIdentifiers) {
                    try {
                        Object obj = null;
                        AuditQuery query = auditReader.createQuery().forRevisionsOfEntity(entityClass, false, true).add(AuditEntity.id().eq(id));
                        if (prop != null) {
                            obj = query.add(AuditEntity.property(prop).maximize().add(AuditEntity.property(prop).le(date))).getSingleResult();
                        }
                        else {
                            query.addOrder(AuditEntity.revisionNumber().desc());
                            List resultList = query.getResultList();
                            for (Object o : resultList) {
                                Object[] singleResult = (Object[]) o;
                                Object revisionEntity = singleResult[1];
                                long timestamp;
                                if (revisionEntity instanceof DefaultRevisionEntity) {
                                    timestamp = ((DefaultRevisionEntity) revisionEntity).getTimestamp();
                                }
                                else {
                                    timestamp = ((RevisionDomainEntity) revisionEntity).getTimestamp();
                                }
                                if (timestamp <= date.getTime()) {
                                    obj = o;
                                    break;
                                }
                            }
                        }
                        if (obj != null) {
                            Object[] singleResult = (Object[]) obj;
                            T entity = (T) singleResult[0];
                            entity.forceAttributesLoad();
                            result.put(id, new RevisionData<T>(entity, singleResult[1], CrudType.from((RevisionType) singleResult[2])));
                        }
                    }
                    catch (javax.persistence.NoResultException e) {
                        logger.debug("unable to find revision data by[entity_class=%s,date=%s,unique_identifier=%s]", entityClass.getName(), date, id);
                    }
                }
                return result;
            }
        });
    }

    @Override
    public <T extends AbstractPersistentEntity> RevisionData<T> latestRevisionForDate(Class<T> entityClass,
                                                                                      Date date,
                                                                                      long uniqueIdentifier,
                                                                                      String prop)
    {
        Map<Long, RevisionData<T>> result = latestRevisionForDate(entityClass, date, Collections.singleton(uniqueIdentifier), prop);
        if (!result.isEmpty()) { return result.values().iterator().next(); }
        return null;
    }

    @Required
    @Autowired
    @Qualifier(IBeanNameReferences.HIBERNATE_TEMPLATE)
    public void setHibernateTemplate(LocalHibernateTemplate hibernateTemplate)
    {
        this.hibernateTemplate = hibernateTemplate;
    }

    @Override
    public IHibernateOperations getLocalHibernateTemplate()
    {
        return hibernateTemplate;
    }
}
